第14章 引导

事实上,编译好的可执行文件真正的执行入口并非我们所写的main.main函数,因为编译器总是会插入一段引导代码,完成诸如命令行参数、运行时初始化等工作,然后才会进入用户逻辑。

要从src/runtime目录下的一堆文件中找到真正的入口,其实很容易。随便准备一个编译好的目标文件,比如“Hello,World!”。

test.go

package main

func main() { println(“hello,world!“) }

编译,然后用GDB查看。

建议:尽可能使用命令行编译,而不是某些IDE的菜单命令,这有助于我们熟悉各种编译开关参数的具体功能。其次,调试程序时,建议使用-gcflags”-N-l”参数关闭编译器代码优化和函数内联,避免断点和单步执行无法准确对应源码行,避免小函数和局部变量被优化掉。

$go build-gcflags”-N-l” -o test test.go

如果在平台使用交叉编译(Cross Compile),需要设置GOOS环境变量。

$gdb test

(gdb)info files Local exec file: Entry point:0x44dd00

(gdb)b*0x44dd00 Breakpoint 1 at 0x44dd00:file/usr/local/go/src/runtime/rt0_linux_amd64.s,line 8.

很简单,找到真正的入口地址,然后利用断点命令就可以轻松找到目标源文件信息。

在src/runtime目录下有很多不同平台的入口文件,都由汇编实现。

$ls rt0_* rt0_android_arm.s rt0_dragonfly_amd64.s rt0_linux_amd64.s … rt0_darwin_386.s rt0_freebsd_386.s rt0_linux_arm.s … rt0_darwin_amd64.s rt0_freebsd_amd64.s rt0_linux_arm64.s …

用你习惯的代码编辑器打开源文件,跳转到指定行,查看具体内容。

rt0_linux_amd64.s

TEXT_rt0_amd64_linux(SB),NOSPLIT,main(SB),AX JMP AX

TEXT main(SB),NOSPLIT,runtime·rt0_go(SB),AX JMP AX

用GDB设置断点命令看看这个rt0_go在哪儿。

注意:源码文件中的“?”符号编译后变成正常的“.”。

(gdb)b runtime.rt0_go Breakpoint 2 at 0x44a780:file/usr/local/go/src/runtime/asm_amd64.s,line 12.

这段汇编代码就是你真正要找的目标,正是它完成了初始化和运行时启动。

asm_amd64.s

TEXT runtime·rt0_go(SB),NOSPLIT,$0

... 

// 调用初始化函数 CALL runtime·args(SB) CALL runtime·osinit(SB) CALL runtime·schedinit(SB)

// 创建main goroutine用于执行runtime.main

MOVQ 0
CALL runtime·newproc(SB) POPQ AX POPQ AX

// 让当前线程开始执行main goroutine

CALL runtime·mstart(SB)

RET

DATA runtime·mainPC+0(SB)/8,8

至此,由汇编语言针对特定平台实现的引导过程就全部完成。后续内容基本上都是由Go代码实现的。

(gdb)b runtime.main Breakpoint 3 at 0x423250:file/usr/local/go/src/runtime/proc.go,line 28.